drive_v3/
credentials.rs

1use std::{fs, path::Path};
2use serde::{Deserialize, Serialize};
3
4use crate::{Error, ErrorKind};
5use crate::AccessToken;
6use crate::ClientSecrets;
7
8/// Contains the credentials ( [`ClientSecrets`](crate::ClientSecrets) and
9/// [`AccessToken`](crate::AccessToken) ) for a authorizing requests to the Google Drive API using
10/// [OAuth 2.0](https://developers.google.com/identity/protocols/oauth2).
11///
12/// # Note:
13///
14/// When creating credentials a `client_secrets` JSON file is required in order to use OAuth2 to authorize your API.
15///
16/// If you do not have a `client_secrets` JSON file, follow
17/// [these steps](https://developers.google.com/drive/api/quickstart/python#set_up_your_environment), once you complete the
18/// [Authorize credentials for a desktop application](https://developers.google.com/drive/api/quickstart/python#authorize_credentials_for_a_desktop_application)
19/// section you can use the downloaded secrets file.
20///
21/// # Examples
22///
23/// ```no_run
24/// use drive_v3::Credentials;
25/// # use drive_v3::Error;
26///
27/// let secrets_path = "my_client_secrets.json";
28/// # let secrets_path = "../.secure-files/google_drive_secrets.json";
29/// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
30///
31/// let credentials = Credentials::from_client_secrets_file(&secrets_path, &scopes)?;
32///
33/// assert!( credentials.are_valid() );
34/// # Ok::<(), Error>(())
35/// ```
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct Credentials {
38    /// Representation of the `client_secret.json` file that contains the authorization info for your API.
39    pub client_secrets: ClientSecrets,
40
41    /// Token used to authenticate and provide authorization information to Google APIs.
42    pub access_token: AccessToken,
43}
44
45impl Credentials {
46    /// Creates new [`Credentials`] using `client_secrets` and `access_token`.
47    ///
48    /// Examples:
49    ///
50    /// ```no_run
51    /// use drive_v3::AccessToken;
52    /// use drive_v3::{Credentials, Drive};
53    /// use drive_v3::ClientSecrets;
54    /// # use drive_v3::Error;
55    ///
56    /// // Load your client_secrets file
57    /// let secrets_path = "my_client_secrets.json";
58    /// # let secrets_path = "../.secure-files/google_drive_secrets.json";
59    /// let my_client_secrets = ClientSecrets::from_file(secrets_path)?;
60    ///
61    /// // Request an access token, this will prompt you to authorize via the browser
62    /// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
63    /// let my_access_token = AccessToken::request(&my_client_secrets, &scopes)?;
64    ///
65    /// // Create Credentials
66    /// let my_credentials = Credentials::new(&my_client_secrets, &my_access_token);
67    ///
68    /// // now you can create a Drive to make requests
69    /// let drive = Drive::new(&my_credentials);
70    /// let files = drive.files.list();
71    ///
72    /// println!("files in drive: {:#?}", files);
73    ///
74    /// # Ok::<(), Error>(())
75    /// ```
76    pub fn new( client_secrets: &ClientSecrets, access_token: &AccessToken ) -> Self {
77        Self {
78            client_secrets: client_secrets.clone(),
79            access_token: access_token.clone(),
80        }
81    }
82
83    /// Gets the [access token](https://cloud.google.com/docs/authentication/token-types#access) which is sent as a
84    /// [HTTP Authorization request header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) to the
85    /// Google Drive API to authenticate access.
86    ///
87    /// Examples:
88    ///
89    /// ```no_run
90    /// # use drive_v3::Credentials;
91    /// # use drive_v3::Error;
92    /// #
93    /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
94    /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
95    /// #
96    /// # let mut credentials = Credentials::from_file(&credentials_path, &scopes)?;
97    /// #
98    /// // You can use this token to make a requests (with reqwest for example) using it for authorization
99    /// let client = reqwest::blocking::Client::new();
100    ///
101    /// let body = client.get("google-api-endpoint")
102    ///     .bearer_auth( &credentials.get_access_token() )
103    ///     .send()?
104    ///     .text()?;
105    ///
106    /// println!("response: {:?}", body);
107    /// # Ok::<(), Error>(())
108    /// ```
109    pub fn get_access_token( &self ) -> String {
110        self.access_token.access_token.to_string()
111    }
112
113    /// Saves [`Credentials`] as a JSON file in `path`.
114    ///
115    /// # Examples
116    ///
117    /// ```rust
118    /// use drive_v3::Credentials;
119    /// #
120    /// # use drive_v3::Error;
121    /// #
122    /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
123    /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
124    /// #
125    /// # let mut credentials = Credentials::from_file(&credentials_path, &scopes)?;
126    ///
127    /// // Store credentials for the next time you need authorization
128    /// credentials.store("credentials_new.json")?;
129    ///
130    /// let required_scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
131    /// let stored_credentials = Credentials::from_file("credentials_new.json", &required_scopes)?;
132    ///
133    /// assert_eq!(credentials, stored_credentials);
134    /// # Ok::<(), Error>(())
135    /// ```
136    ///
137    /// # Errors
138    ///
139    /// - a [`Json`](crate::ErrorKind::Json) error, if unable to parse the [`Credentials`] to JSON.
140    /// - an [`IO`](crate::ErrorKind::IO) error, if unable to write to `path`.
141    pub fn store<T: AsRef<Path>> ( &self, path: T ) -> crate::Result<()> {
142        let contents = serde_json::to_string(&self)?;
143        fs::write(&path, contents)?;
144
145        Ok(())
146    }
147
148    /// Refreshes expired or invalid [`Credentials`].
149    ///
150    /// # Note
151    ///
152    /// The refreshed credentials are updated in place, you should save them to a file using the [`store`](Credentials::store)
153    /// function to avoid requesting to many new credentials.
154    ///
155    /// # Examples
156    ///
157    /// ```no_run
158    /// # use drive_v3::{Credentials, Error};
159    /// #
160    /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
161    /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
162    /// #
163    /// # let mut credentials = Credentials::from_file(&credentials_path, &scopes)?;
164    /// #
165    /// if !credentials.are_valid() {
166    ///     credentials.refresh()?;
167    /// }
168    ///
169    /// assert!( credentials.are_valid() );
170    /// # Ok::<(), Error>(())
171    /// ```
172    ///
173    /// # Errors
174    ///
175    /// - a [`Request`](crate::ErrorKind::Request) error, if unable to send the refresh request or get a body from the response.
176    /// - a [`Json`](crate::ErrorKind::Json) error, if unable to parse the received token from JSON.
177    pub fn refresh( &mut self ) -> crate::Result<()> {
178        self.access_token.refresh( &self.client_secrets )
179    }
180
181    /// Checks if these [`Credentials`] are valid by making a request to the
182    /// [tokeninfo](https://developers.google.com/identity/sign-in/web/backend-auth#calling-the-tokeninfo-endpoint) endpoint of
183    /// the Google Drive API.
184    ///
185    /// # Note
186    ///
187    /// [`are_valid`](Credentials::are_valid) will return false if the credentials have expired or have been revoked, but it will
188    /// also return false if the tokeninfo endpoint cannot be reached or if the request returned an error response of any kind.
189    ///
190    /// # Examples
191    ///
192    /// ```rust
193    /// # use drive_v3::{Credentials, Error};
194    /// #
195    /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
196    /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
197    /// #
198    /// # let credentials = Credentials::from_file(&credentials_path, &scopes)?;
199    /// #
200    /// if credentials.are_valid() {
201    ///     println!("credentials are valid!, you can start making requests");
202    /// } else {
203    ///     println!("credentials are invalid :c, try calling 'credentials.refresh()' to update them");
204    /// }
205    /// # Ok::<(), Error>(())
206    /// ```
207    pub fn are_valid( &self ) -> bool {
208        self.access_token.is_valid()
209    }
210
211    /// Retrieves previously stored [`Credentials`] from `file` and checks that they have the required `scopes` enabled.
212    ///
213    /// # Note
214    ///
215    /// [`from_file`](Credentials::from_file) only parses the stored credentials it does not validate them, before using them to
216    /// make requests you must ensure that they are still valid using the [`are_valid`](Credentials::are_valid) function.
217    /// If they are no longer valid use the [`refresh`](Credentials::refresh) function to update them.
218    ///
219    /// # Examples
220    ///
221    /// ```rust
222    /// use drive_v3::Credentials;
223    /// # use drive_v3::Error;
224    ///
225    /// let credentials_path = "my_credentials.json";
226    /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
227    /// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
228    ///
229    /// let credentials = Credentials::from_file(&credentials_path, &scopes)?;
230    /// # Ok::<(), Error>(())
231    /// ```
232    ///
233    /// # Errors
234    ///
235    /// - an [`IO`](crate::ErrorKind::IO) error, if the file does not exist.
236    /// - a [`Json`](crate::ErrorKind::Json) error, if parsing of the file from JSON failed.
237    /// - a [`MismatchedScopes`](crate::ErrorKind::MismatchedScopes) error, if the scopes in the access token are different
238    /// to the `scopes` passed to the function.
239    pub fn from_file<T, U> ( file: T, scopes: &[U] ) -> crate::Result<Self>
240        where
241            T: AsRef<Path>,
242            U: AsRef<str>,
243    {
244        let contents = fs::read_to_string(&file)?;
245        let credentials: Credentials = serde_json::from_str(&contents)?;
246
247        if !credentials.access_token.has_scopes(scopes) {
248            return Err(
249                Error::new(ErrorKind::MismatchedScopes, "stored access token does not contain the requested scopes")
250            )
251        }
252
253        Ok(credentials)
254    }
255
256    /// Attempts to get [`Credentials`] by prompting the user for authorization.
257    ///
258    /// # Note:
259    ///
260    /// A `client_secrets` json file is required in order to use OAuth2 to authorize your API.
261    ///
262    /// If you do not have a `client_secrets` json file, follow
263    /// [these steps](https://developers.google.com/drive/api/quickstart/python#set_up_your_environment), once you complete the
264    /// [Authorize credentials for a desktop application](https://developers.google.com/drive/api/quickstart/python#authorize_credentials_for_a_desktop_application)
265    /// section you can use the downloaded secrets file.
266    ///
267    /// # Examples
268    ///
269    /// ```no_run
270    /// use drive_v3::Credentials;
271    /// # use drive_v3::Error;
272    ///
273    /// let secrets_path = "my_client_secrets.json";
274    /// # let secrets_path = "../.secure-files/google_drive_secrets.json";
275    /// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
276    ///
277    /// let credentials = Credentials::from_client_secrets_file(&secrets_path, &scopes)?;
278    ///
279    /// assert!( credentials.are_valid() );
280    /// # Ok::<(), Error>(())
281    /// ```
282    ///
283    /// # Errors
284    ///
285    /// - an [`IO`](crate::ErrorKind::IO) or [`Json`](crate::ErrorKind::Json) error, if unable get
286    /// [`ClientSecrets`](crate::ClientSecrets) from `file`.
287    /// - a [`HexDecoding`](crate::ErrorKind::HexDecoding), [`UrlParsing`](crate::ErrorKind::UrlParsing),
288    /// [`Request`](crate::ErrorKind::Request) or [`Response`](crate::ErrorKind::Response) error, if the
289    /// get access token request fails.
290    /// - a [`MismatchedScopes`](crate::ErrorKind::MismatchedScopes) error, if the scopes in the created access token are
291    /// different to the `scopes` passed to the function.
292    #[cfg(not(tarpaulin_include))]
293    pub fn from_client_secrets_file<T, U> ( file: T, scopes: &[U] ) -> crate::Result<Self>
294        where
295            T: AsRef<Path>,
296            U: AsRef<str>,
297    {
298        let client_secrets = ClientSecrets::from_file(&file)?;
299        let access_token = AccessToken::request(&client_secrets, scopes)?;
300
301        Ok( Self::new(&client_secrets, &access_token) )
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use serde_json;
308    use super::ErrorKind;
309    use super::Credentials;
310    use std::process::Command;
311    use testfile::{self, TestFile};
312    use crate::utils::test::{VALID_CREDENTIALS, VALID_SECRETS};
313
314    fn get_test_json() -> String {
315        String::from("{
316            \"client_secrets\": {
317                \"client_id\": \"test\",
318                \"project_id\": \"test\",
319                \"auth_uri\": \"test\",
320                \"token_uri\": \"test\",
321                \"auth_provider_x509_cert_url\": \"test\",
322                \"client_secret\": \"test\",
323                \"redirect_uris\": [\"test\"]
324            },
325            \"access_token\": {
326                \"access_token\": \"test\",
327                \"expires_in\": 0,
328                \"refresh_token\": \"test\",
329                \"scope\": \"test-scope other-test-scope unchecked-scope\",
330                \"token_type\": \"test\"
331            }
332        }")
333    }
334
335    fn get_test_scopes() -> Vec<String> {
336        vec![
337            String::from("test-scope"),
338            String::from("other-test-scope"),
339        ]
340    }
341
342    fn get_test_credentials_file() -> TestFile {
343        testfile::from( &get_test_json() )
344    }
345
346    #[test]
347    fn new_test() {
348        let client_secrets = VALID_SECRETS.clone();
349        let access_token = VALID_CREDENTIALS.access_token.clone();
350
351        let credentials = Credentials::new(&client_secrets, &access_token);
352
353        assert_eq!(credentials.client_secrets, client_secrets);
354        assert_eq!(credentials.access_token, access_token);
355    }
356
357    #[test]
358    fn get_access_token_test() {
359        let credentials = VALID_CREDENTIALS.clone();
360
361        assert_eq!( credentials.access_token.access_token, credentials.get_access_token() );
362    }
363
364    #[test]
365    fn refresh_test() {
366        let mut credentials = VALID_CREDENTIALS.clone();
367        let result = credentials.refresh();
368
369        assert!( result.is_ok() );
370    }
371
372    #[test]
373    fn store_test() {
374        let scopes: [&str; 0] = [];
375        let credentials = VALID_CREDENTIALS.clone();
376
377        credentials.store("test-stored-credentials.json").unwrap();
378
379        let saved_credentials = Credentials::from_file("test-stored-credentials.json", &scopes).unwrap();
380
381        Command::new("rm").arg("test-stored-credentials.json").output().unwrap();
382
383        assert_eq!(saved_credentials, credentials);
384    }
385
386    #[test]
387    fn are_valid_test() {
388        let scopes = get_test_scopes();
389        let test_file = get_test_credentials_file();
390
391        let credentials = Credentials::from_file(&test_file, &scopes).unwrap();
392
393        assert!( !credentials.are_valid() );
394    }
395
396    #[test]
397    fn from_file_test() {
398        let scopes = get_test_scopes();
399        let test_file = get_test_credentials_file();
400        let expected_credentials = serde_json::from_str( &get_test_json() ).unwrap();
401
402        let credentials = Credentials::from_file(&test_file, &scopes).unwrap();
403
404        assert_eq!(credentials, expected_credentials);
405    }
406
407    #[test]
408    fn from_file_invalid_scopes_test() {
409        let scopes = ["invalid-scope"];
410        let test_file = get_test_credentials_file();
411
412        let result = Credentials::from_file(&test_file, &scopes);
413
414        assert!( result.is_err() );
415        assert_eq!( result.unwrap_err().kind, ErrorKind::MismatchedScopes );
416    }
417
418    #[test]
419    fn from_file_non_existent_test() {
420        let scopes = get_test_scopes();
421        let test_file = "non-existent.json";
422
423        let result = Credentials::from_file(&test_file, &scopes);
424
425        assert!( result.is_err() );
426        assert_eq!( result.unwrap_err().kind, ErrorKind::IO );
427    }
428
429    #[test]
430    fn from_file_invalid_json_test() {
431        let scopes = get_test_scopes();
432        let test_file =  testfile::from("This is not JSON");
433
434        let result = Credentials::from_file(&test_file, &scopes);
435
436        assert!( result.is_err() );
437        assert_eq!( result.unwrap_err().kind, ErrorKind::Json );
438    }
439
440    #[test]
441    fn from_file_no_longer_valid_test() {
442        let scopes = get_test_scopes();
443        let test_file = get_test_credentials_file();
444
445        let credentials = Credentials::from_file(&test_file, &scopes).unwrap();
446
447        assert_eq!( credentials.are_valid(), false );
448    }
449}